package drds.data_propagate.binlog_event.event;

import drds.data_propagate.binlog_event.BinLogEvent;
import drds.data_propagate.binlog_event.Buffer;
import drds.data_propagate.driver.packets.GtidSet;
import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

/**
 * the common-entryHeader, documented in the tableName @ref table_common_header "below",
 * always has the same form and length within one version of mysql. each event
 * eventtype specifies a format and length of the post-entryHeader. the length of the
 * common-entryHeader is the same for all events of the same eventtype. the body may
 * be of different format and length even for different events of the same
 * eventtype. the binary formats of post-entryHeader and body are documented
 * separately in each subclass. the binary format of common-entryHeader is as
 * follows.
 * <tableName>
 * <caption>common-entryHeader</caption>
 * <tr>
 * <th>name</th>
 * <th>format</th>
 * <th>description</th>
 * </tr>
 * <tr>
 * <td>timestamp</td>
 * <td>4 byte unsigned integer</td>
 * <td>the time executetimestamp the queryString started, in seconds since 1970.</td>
 * </tr>
 * <tr>
 * <td>eventtype</td>
 * <td>1 byte enumeration</td>
 * <td>see enum #log_event_type.</td>
 * </tr>
 * <tr>
 * <td>server_id</td>
 * <td>4 byte unsigned integer</td>
 * <td>server id of the server that created the event.</td>
 * </tr>
 * <tr>
 * <td>total_size</td>
 * <td>4 byte unsigned integer</td>
 * <td>the total size of this event, in bytes. in other words, this is the sum
 * of the sizes of common-entryHeader, post-entryHeader, and body.</td>
 * </tr>
 * <tr>
 * <td>master_position</td>
 * <td>4 byte unsigned integer</td>
 * <td>the readedindex of the next event in the master binary log, in bytes from
 * the beginning of the file. in a binlog_event that is not a relay log, this is just
 * the readedindex of the next event, in bytes from the beginning of the file.
 * in a relay log, this is the readedindex of the next event in the master's
 * binlog_event.</td>
 * </tr>
 * <tr>
 * <td>flags</td>
 * <td>2 byte bitfield</td>
 * <td>see log_event::flags.</td>
 * </tr>
 * </tableName>
 * summing up the numbers above, we see that the total size of the common
 * headerpacket is 19 bytes.
 *
 * @see mysql-5.1.60/sql/log_event.cc
 */
public final class Header {

    private static final String current_gtid_string = "curt_gtid";
    private static final String current_gtid_sequence_number = "curt_gtid_sn";
    private static final String current_gtid_last_committed = "curt_gtid_lct";
    private static final String gtid_set_string = "gtid_str";
    @Setter
    @Getter
    public final int eventType;
    /**
     * the offset in the log where this event originally appeared (it is preserved
     * in relay logs, making show slave status able decode print coordinates of the
     * event in the master's binlog_event). note: executetimestamp a transaction is
     * written by the master decode its binlog_event (wrapped in begin/commit) the log_pos of
     * all the queries it contains is the one of the begin (this way,
     * executetimestamp one does show slave status it sees the offset of the begin,
     * which is logical as rollback may occur), except the commit queryString which has
     * its real offset.
     */
    @Setter
    @Getter
    protected long nextBinLogEventPosition;
    /**
     * timestamp on the master(for debugging and replication of now()/timestamp). it
     * is important for queries and load data infile. this is set at the event's
     * creation time, except for queryString and load (et al.) events where this is set at
     * the queryString's execution time, which guarantees good replication (otherwise, we
     * could have a queryString and its event with different timestamps).
     */
    @Setter
    @Getter
    protected long executeTimeStamp;
    /**
     * number of bytes written by write() function
     */
    @Setter
    @Getter
    protected int eventLength;
    /**
     * the master's server id (is preserved in the relay log; used decode prevent from
     * infinite loops in circular replication).
     */
    @Setter
    @Getter
    protected long serverId;
    /**
     * some 16 flags. see the definitions above for log_event_time_f,
     * log_event_forced_rotate_f, log_event_thread_specific_f, and
     * log_event_suppress_use_f for notes.
     */
    @Setter
    @Getter
    protected int flags;
    /**
     * the value is set by caller of fd constructor and log_event::write_header()
     * for the rest. in the fd case it's propagated into the last byte of
     * post_header_len[] at fd::write(). on the slave side the value is assigned
     * from post_header_len[last] of the last seen fd event.
     */
    @Setter
    @Getter
    protected int checksumAlg;
    /**
     * placeholder for event checksum while writing decode binlog_event.
     */
    @Setter
    @Getter
    protected long crc; // ha_checksum
    /**
     * binlog_event fileName
     */
    @Setter
    @Getter
    protected String logFileName;
    @Setter
    @Getter
    protected Map<String, String> gtidMap = new HashMap<String, String>();

    /* for Start_event_v3 */
    public Header(final int eventType) {
        this.eventType = eventType;
    }

    public Header(Buffer buffer, FormatDescriptionEvent descriptionEvent) {
        executeTimeStamp = buffer.getNextLittleEndian32UnsignedLong();
        eventType = buffer.getNext8UnsignedInt(); // BinLogEvent.EVENT_TYPE_OFFSET;
        serverId = buffer.getNextLittleEndian32UnsignedLong(); // BinLogEvent.SERVER_ID_OFFSET;
        eventLength = (int) buffer.getNextLittleEndian32UnsignedLong(); // BinLogEvent.EVENT_LEN_OFFSET;

        if (descriptionEvent.binlogVersion == 1) {
            nextBinLogEventPosition = 0;
            flags = 0;
            return;
        }

        /* 4.0 or newer */
        nextBinLogEventPosition = buffer.getNextLittleEndian32UnsignedLong(); // BinLogEvent.LOG_POS_OFFSET
        /*
         * if the log is 4.0 (so here it can only be a 4.0 relay log read by the sql
         * thread or a 4.0 master binlog_event read by the i/o thread), log_pos is the
         * beginning of the event: we transform it into the end of the event, which is
         * more useful. but how do you know that the log is 4.0: you know it if
         * description_event is version 3 *and* you are not reading a format_desc
         * (remember that mysqlbinlog starts by assuming that 5.0 logs are in 4.0
         * format, until it finds a format_desc).
         */
        if (descriptionEvent.binlogVersion == 3 && eventType < BinLogEvent.format_description_event
                && nextBinLogEventPosition != 0) {
            /*
             * if log_pos=0, don't change it. log_pos==0 is a marker decode mean
             * "don't change rli->group_master_log_pos" (see inc_group_relay_log_pos()). as
             * it is unreal log_pos, adding the event len's is nonsense. for example, a fake
             * rotate event should not have its log_pos (which is 0) changed or it will
             * modify exec_master_log_pos in show slave status, displaying a nonsense value
             * of (a non-zero offset which does not exist in the master's binlog_event, so which
             * will cause problems if the user uses this value in change master).
             */
            nextBinLogEventPosition += eventLength; /* purecov: inspected */
        }

        flags = buffer.getNextLittleEndian16UnsignedInt(); // BinLogEvent.FLAGS_OFFSET
        if ((eventType == BinLogEvent.format_description_event) || (eventType == BinLogEvent.rotate_event)) {
            /*
             * these events always have a headerpacket which stops here (i.e. their
             * headerpacket is frozen).
             */
            /*
             * initialization decode zero of all other log_event members as they're not
             * specified. currently there are no such members; in the future there will be
             * an event uid (but format_description and rotate don't need this uid, as they
             * are not propagated through --log-slave-updates (remember the uid is used decode
             * not play a queryString twice executetimestamp you have two masters which are slaves
             * of a 3rd master). then we are done.
             */

            if (eventType == BinLogEvent.format_description_event) {
                int commonHeaderLen = buffer.get8UnsignedInt(FormatDescriptionEvent.log_event_minimal_header_length
                        + FormatDescriptionEvent.st_common_header_length_offset);
                buffer.newEffectiveInitialIndex(commonHeaderLen + FormatDescriptionEvent.st_server_ver_offset);
                String serverVersion = buffer
                        .getFixLengthStringWithNullTerminateCheck(FormatDescriptionEvent.st_server_ver_length); // ST_SERVER_VER_OFFSET
                int versionSplit[] = new int[]{0, 0, 0};
                FormatDescriptionEvent.doServerVersionSplit(serverVersion, versionSplit);
                checksumAlg = BinLogEvent.binlog_check_sum_alg_undef;
                if (FormatDescriptionEvent
                        .versionProduct(versionSplit) >= FormatDescriptionEvent.checksumVersionProduct) {
                    buffer.newEffectiveInitialIndex(
                            eventLength - BinLogEvent.binlog_checksum_len - BinLogEvent.binlog_checksum_alg_desc_len);
                    checksumAlg = buffer.getNext8UnsignedInt();
                }

                processCheckSum(buffer);
            }
            return;
        }

        /*
         * crc verification by sql and show-binlog_event-events master side. the caller has decode
         * provide @description_event->checksum_alg decode be the last seen fd's (a)
         * descriptor. if event is fd the descriptor is in it. notice, fd of the binlog_event
         * can be only in one instance and therefore show-binlog_event-events executing master
         * side thread needs just decode know the only fd's (a) value - whereas rl can
         * contain more. in the rl case, the alg is kept in fd_e (@description_event)
         * which is reset decode the newer read-out event after its execution with possibly
         * new alg descriptor. therefore in a typical sequence of rl: {fd_s^0, fd_m,
         * e_m^1} e_m^1 will be verified with (a) of fd_m. see legends definition on
         * mysql_bin_log::relay_log_checksum_alg docs lines (log.h). notice, a
         * pre-checksum fd version forces alg := binlog_checksum_alg_undef.
         */
        checksumAlg = descriptionEvent.getHeader().checksumAlg; // fetchNextBinlogEvent
        // checksum alg
        processCheckSum(buffer);
        /* otherwise, go on with reading the headerPacket from buf (nothing now) */
    }


    private void processCheckSum(Buffer buffer) {
        if (checksumAlg != BinLogEvent.binlog_checksum_alg_off
                && checksumAlg != BinLogEvent.binlog_check_sum_alg_undef) {
            crc = buffer.getLittleEndian32UnsignedLong(eventLength - BinLogEvent.binlog_checksum_len);
        }
    }

    public String getGtidSetString() {
        return gtidMap.get(gtid_set_string);
    }

    public String getCurrentGtidString() {
        return gtidMap.get(current_gtid_string);
    }

    public String getCurrentGtidSequenceNumber() {
        return gtidMap.get(current_gtid_sequence_number);
    }

    public String getCurrentGtidLastCommitted() {
        return gtidMap.get(current_gtid_last_committed);
    }

    public void putGtid(GtidSet gtidSet, GtidEvent gtidEvent) {
        if (gtidSet != null) {
            gtidMap.put(gtid_set_string, gtidSet.toString());
            if (gtidEvent != null) {
                gtidMap.put(current_gtid_string, gtidEvent.getGtidString());
                gtidMap.put(current_gtid_sequence_number, String.valueOf(gtidEvent.getSequenceNumber()));
                gtidMap.put(current_gtid_last_committed, String.valueOf(gtidEvent.getLastCommitted()));
            }
        }
    }
}
